package com.sunday.redisson.codec;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.ByteBufOutputStream;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.redisson.client.codec.BaseCodec;
import org.redisson.client.protocol.Decoder;
import org.redisson.client.protocol.Encoder;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * redisson gson codec
 * https://github.com/redisson/redisson/issues/3092
 *
 * @author zsy
 * @since 2022/10/20
 */
@Slf4j
public class GsonCodec extends BaseCodec {

    public GsonCodec() {
        this(new Gson());
    }

    public GsonCodec(Gson gson) {
        this.gson = gson;
    }

    @Getter
    private Gson gson;

    private final Map<String, Class<?>> classMap = new ConcurrentHashMap<>();

    public void mapClass(Class<?> clazz) {
        classMap.put(clazz.getName(), clazz);
    }

    private final Encoder encoder = in -> {
        ByteBuf out = ByteBufAllocator.DEFAULT.buffer();
        try (ByteBufOutputStream os = new ByteBufOutputStream(out);
             OutputStreamWriter writer = new OutputStreamWriter(os)) {
            JsonElement jsonElement = gson.toJsonTree(in);
            if (jsonElement.isJsonObject()) {
                jsonElement.getAsJsonObject().addProperty("@class", in.getClass().getName());
            }
            if (in instanceof String) {
                writer.write(String.valueOf(in));
            } else {
                writer.write(gson.toJson(jsonElement));
            }
            return os.buffer();
        } catch (IOException e) {
            out.release();
            throw e;
        } catch (Exception e) {
            out.release();
            throw new IOException(e);
        }
    };

    private final Decoder<Object> decoder = (buf, state) -> {
        try (ByteBufInputStream byteBufInputStream = new ByteBufInputStream(buf);
             InputStreamReader inStream = new InputStreamReader(byteBufInputStream, "UTF-8");
             BufferedReader in = new BufferedReader(inStream)) {
            String line = in.readLine();
            JsonElement jsonElement = JsonParser.parseString(line);
            if (!jsonElement.isJsonObject()) {
                return line;
            }
            String type = jsonElement.getAsJsonObject().get("@class").getAsString();
            return gson.fromJson(jsonElement, getClassFromType(type));
        }
    };

    public Class<?> getClassFromType(String name) {
        Class<?> clazz = classMap.get(name);
        if (clazz == null) {
            try {
                clazz = Class.forName(name);
                classMap.put(name, clazz);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        if (clazz != null) return clazz;
        throw new RuntimeException(STR."Could not find a class named: \{name}");
    }

    @Override
    public Decoder<Object> getValueDecoder() {
        return decoder;
    }

    @Override
    public Encoder getValueEncoder() {
        return encoder;
    }

    @Override
    public ClassLoader getClassLoader() {
        if (gson.getClass().getClassLoader() != null) {
            return gson.getClass().getClassLoader();
        }
        return super.getClassLoader();
    }

}